// Canvas 处理事件集合类
class CanvasHandler {
  constructor(shape) {
    this.shape = shape
  }

  mousedown(e) {
    const self = this
    this.shape.setRefresh(true)
    const rect = cvs.getBoundingClientRect()
    const clickX = e.clientX - rect.left
    const clickY = e.clientY - rect.top
    let distance = [0, 0]

    let clickedShape = this.shape.position(clickX, clickY)
    console.log("%c Line:102 🥔 选中的图形", "color:#3f7cff", clickX, clickY, clickedShape)

    clickedShape
      ? moveShape(clickX, clickY, rect, clickedShape)
      : createShape(clickX, clickY, rect)
    clearMouseUpEvent()

    function moveShape(clickX, clickY, rect, target) {
      const { startX, startY, endX, endY } = target
      window.onmousemove = (e) => {
        const moveX = e.clientX - rect.left - clickX
        const moveY = e.clientY - rect.top - clickY
        distance = [moveX, moveY]
        if (e.altKey) {
          const instance = target.clone()
          // 复制：alt + move
          self.shape.insert(self.shape.getIndex(target), instance)
        } else {
          // move
          target.startX = startX + moveX
          target.startY = startY + moveY
          target.endX = endX + moveX
          target.endY = endY + moveY
        }
      }
    }
    function createShape(clickX, clickY, rect) {
      const className = toolbar.name
      if (!(className in window)) return
      const graph = new window[className](clickX, clickY)
      graph.E = e

      window.onmousemove = (e) => {
        const moveX = e.clientX - rect.left
        const moveY = e.clientY - rect.top
        graph.endX = moveX
        graph.endY = moveY
        if (!self.shape.has(graph)) {
          self.shape.push(graph)
          self.shape.setActive(graph)
        }

        if (graph.selector) {
          // 框选：获取选中的元素
          self.shape.select()
          return
        }

        graph.shiftGraph(graph)
        // if (e.shiftKey) {
        //   let newX, newY
        //   const w = Math.max(graph.width, graph.height)
        //   if (graph.endX < graph.startX && graph.endY <= graph.startY) {
        //     newX = graph.startX - w
        //     newY = graph.startY - w
        //   }
        //   else if (graph.endX <= graph.startX && graph.endY > graph.startY) {
        //     newX = graph.startX - w
        //     newY = graph.startY + w
        //   }
        //   else if (graph.endX > graph.startX && graph.endY >= graph.startY) {
        //     newX = graph.startX + w
        //     newY = graph.startY + w
        //   }
        //   else if (graph.endX >= graph.startX && graph.endY < graph.startY) {
        //     newX = graph.startX + w
        //     newY = graph.startY - w
        //   }
        //   graph.endX = newX
        //   graph.endY = newY
        //   return
        // }
      }
    }
    function clearMouseUpEvent() {
      const active = self.shape.getActive()
      window.onmouseup = () => {
        if (self.shape.selector) {
          self.shape.remove(self.shape.selector)
          self.shape.selector = null

          if (active instanceof window.Group) {
            self.shape.push(active)
            // 移除选中的图形
            self.shape.filter(el => !active.data.includes(el))
          }
        }

        if (active instanceof window.Group) {
          active.move(...distance)
        }
        window.onmousemove = null
        window.onmouseup = null
      }
    }
  }

  mouseleave(e) {
    console.log('=== mouse leave ===')
    this.shape.setRefresh(false)
    window.onmousemove = null
    window.onmouseup = null
    document.onkeydown = null
  }

  dblclick(e) {
    const rect = cvs.getBoundingClientRect()
    const clickX = e.clientX - rect.left
    const clickY = e.clientY - rect.top
    const textbox = this.shape.position(clickX, clickY)
    if (textbox && textbox.isTextbox) {
      const { fontSize, startX, startY, width, height, text } = textbox
      const textarea = document.createElement('textarea')
      textarea.id = `textbox`
      textarea.style.fontSize = `${fontSize}px`
      textarea.style.color = `${textbox.strokeStyle}`
      textarea.style.top = `${startY + rect.top}px`
      textarea.style.left = `${startX + rect.left}px`
      textarea.style.width = `${width + 2 * fontSize}px`
      textarea.style.height = `${height}px`
      textarea.value = text
      document.body.appendChild(textarea)
      textbox.blur(textarea, (e) => {
        this.shape.setRefresh(true)
      })
    }
  }
}

class Shape {
  #data = []
  #active = null
  #refresh = false
  selector = null
  constructor() { }

  // draw all shapes
  draw(ctx, fn) {
    requestAnimationFrame(() => {
      this.draw(ctx, fn)
    })

    if (!this.#refresh) return
    fn && fn()
    this.#data.forEach(shape => {
      if (shape instanceof Group) {
        shape.draw(ctx)
        shape.data.forEach(subShape => subShape.draw(ctx))
      } else {
        shape.draw(ctx)
      }
    })
  }

  empty() {
    return this.#data.length === 0
  }
  has(shape) {
    return this.#data.includes(shape)
  }
  filter(fun) {
    this.#data = this.#data.filter(shape => fun(shape))
  }
  getData() {
    return this.#data
  }
  getCount() {
    return this.#data.length
  }
  setRefresh(bool) {
    return this.#refresh = !!bool
  }

  unshift(shape) {
    if (this.getIndex(shape) > -1) return
    this.#active = shape
    this.#data.unshift(shape)
  }
  push(shape) {
    if (this.has(shape)) return
    if (shape.selector) {
      this.selector = shape
    }
    this.#data.push(shape)
  }
  insert(index, shape) {
    if (this.has(shape)) return
    this.#data.splice(index, 0, shape)
  }
  remove(shape) {
    const index = this.getIndex(shape || this.#active)
    this.#data.splice(index, 1)
  }

  getIndex(shape) {
    return this.#data.indexOf(shape)
  }
  setActive(shape) {
    this.#active = shape
  }
  getActive() {
    return this.#active || this.#data.at(-1)
  }

  position(x, y) {
    const len = this.#data.length
    // 要倒序遍历，否则删除的时候会出问题
    for (let i = len - 1; i >= 0; i--) {
      const shape = this.#data[i]
      if (shape.isInside(x, y)) {
        this.#active = shape
        return this.#active
      }
    }
    return null
  }
  select() {
    const { minX, maxX, minY, maxY } = this.selector

    const callback = (el) => !el.selector && el.minX >= minX && el.maxX <= maxX && el.minY >= minY && el.maxY <= maxY
    const selectMap = this.#data.filter(callback)

    if (selectMap.length > 1) {
      this.#active = new Group(selectMap)
      return this.#active
    }
  }

  down() {
    const shapeIndex = this.getIndex(this.#active)
    if (shapeIndex < 1) return

    const prevShape = this.#data[shapeIndex - 1]
    if (prevShape) {
      this.#data[shapeIndex] = prevShape
      this.#data[shapeIndex - 1] = this.#active
    }
  }

  up() {
    const shapeIndex = this.getIndex(this.#active)
    if (shapeIndex > this.#data.length - 2) return

    const nextShape = this.#data[shapeIndex + 1]
    if (nextShape) {
      this.#data[shapeIndex] = nextShape
      this.#data[shapeIndex + 1] = this.#active
    }
  }

  moveBottom() {
    const index = this.getIndex(this.#active)
    if (index === this.#data.length - 1) {
      this.#data.pop()
    } else {
      this.remove()
    }
    this.unshift(this.#active)
  }

  moveTop() {
    const index = this.getIndex(this.#active)
    if (index === 0) {
      this.#data.shift()
    } else {
      this.remove()
    }
    this.push(this.#active)
  }

  /**
   * 移动图形
   * @param {string} direction up|down|left|right
   * @param {number} step 移动步长
   */
  move(direction, step) {
    const { startX, startY, endX, endY } = this.#active
    if (['up', 'down'].includes(direction)) {
      this.#active.startY = startY + step
      this.#active.endY = endY + step
    } else {
      this.#active.startX = startX + step
      this.#active.endX = endX + step
    }
    const index = this.getIndex(this.#active)
    this.#data[index] = this.#active
  }

}


// 配置项参数
class Config {
  lineWidthMap = {
    'thin': 1.25,
    'bold': 2.50,
    'extraBold': 3.75
  }
  lineDashMap = {
    solid: [],
    dashed: [10, 10],
    dotted: [5, 5],
  }
  fontSizeMap = {
    small: 12,
    medium: 16,
    large: 20,
    extraLarge: 24,
  }
  fontFamilyMap = {
    virgil: 'Virgil',
    normal: 'Arial',
    code: 'Courier New',
  }
  arrow = { left: '', right: '', radius: 8 }
  constructor() {
    this.panel = toolbar.panel
    const [bd, bg] = document.querySelectorAll('.active-color')
    this.strokeStyle = bd.style.getPropertyValue('--swatch-color')
    this.backgroudColor = this.setBackgroudColor(bg)
    this.fill = this.getTragetValue(this.target(3))
    this.lineWidth = this.lineWidthMap[this.getTragetValue(this.target(4))]
    this.lineDash = this.lineDashMap[this.getTragetValue(this.target(5))]
    this.lineStyle = this.getTragetValue(this.target(6))
    this.lineJoin = this.getTragetValue(this.target(7))
    this.lineCap = this.lineJoin === 'round' ? 'round' : 'square'
    this.arrow.left = this.target('input[name=arrowstart]').getAttribute('data-testid')
    this.arrow.right = this.target('input[name=arrowend]').getAttribute('data-testid')
    this.fontSize = this.fontSizeMap[this.getTragetValue(this.target(9))]
    this.fontFamily = this.fontFamilyMap[this.getTragetValue(this.target(10))]
    this.textAlign = this.getTragetValue(this.target(11))
    this.opacity = this.getTragetValue(this.target('.control-label'))

    this.init()
  }

  init() {
    const colors = ['strokeStyle', 'backgroudColor']
    for (const key in colors) {
      const name = colors[key]
      const value = this[name]
      this[name] = this.toRGB(value)
    }
  }

  toRGB(hex) {
    if (hex === 'transparent') return 'transparent'
    if (hex.startsWith('#')) {
      const r = parseInt(hex.slice(1, 3), 16)
      const g = parseInt(hex.slice(3, 5), 16)
      const b = parseInt(hex.slice(5, 7), 16)
      const a = this.opacity / 100
      return `rgba(${r}, ${g}, ${b}, ${a})`
    }
  }

  randomColor() {
    return '#' + Math.random().toString(16).substring(2, 8).padEnd(6, '0')
  }

  setBackgroudColor(el) {
    if (el.classList.contains('is-transparent')) {
      return 'transparent'
    }
    return el.style.getPropertyValue('--swatch-color')
  }

  target(index) {
    if (Number.isInteger(index)) {
      return this.panel.children[index - 1]
    }
    return this.panel.querySelector(index)
  }

  getTragetValue(target) {
    if (target.classList.contains('control-label')) {
      return +target.firstElementChild.value
    }
    const el = target.querySelector('input[type=radio]:checked')
    return el.dataset.testid
  }
}

class Graph extends Config {
  constructor() {
    super()
    this.startX = 0
    this.startY = 0
    this.endX = 0
    this.endY = 0
  }

  get minX() { return Math.min(this.startX, this.endX) }
  get minY() { return Math.min(this.startY, this.endY) }
  get maxX() { return Math.max(this.startX, this.endX) }
  get maxY() { return Math.max(this.startY, this.endY) }

  shiftGraph(graph) {
    const { E, startX, startY, endX, endY, width, height } = graph
    if (!E.shiftKey) return
    let newX, newY
    const w = Math.max(width, height)
    if (endX < startX && endY <= startY) {
      newX = startX - w
      newY = startY - w
    }
    else if (endX <= startX && endY > startY) {
      newX = startX - w
      newY = startY + w
    }
    else if (endX > startX && endY >= startY) {
      newX = startX + w
      newY = startY + w
    }
    else if (endX >= startX && endY < startY) {
      newX = startX + w
      newY = startY - w
    }
    graph.endX = newX
    graph.endY = newY
  }

  isInside(x, y) {
    return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY
  }

  /**
   * 设置线的样式：实线、虚线、点线
   *
   * @param {CanvasRenderingContext2D} ctx
   * @return {void}
   */
  drawLineDash(ctx) {
    ctx.setLineDash(this.lineDash.map(v => Math.max(v, this.lineWidth) * dpr))
  }

  /**
   * 填充或描边
   *
   * @param {CanvasRenderingContext2D} ctx
   * @return {void}
   */
  drawStrokeOrFill(ctx) {
    if (this.backgroudColor === 'transparent') {
      ctx.strokeStyle = this.backgroudColor
      ctx.stroke()
    } else {
      ctx.fillStyle = this.backgroudColor
      ctx.fill()
    }
  }

  /**
   * 描边样式
   *
   * @param {CanvasRenderingContext2D} ctx
   * @return {void}
   */
  drawStrokeStyle(ctx) {
    // 描边
    ctx.strokeStyle = this.strokeStyle
    ctx.lineWidth = this.lineWidth * dpr
    ctx.lineCap = this.lineCap
    ctx.lineJoin = this.lineJoin
    ctx.stroke()
  }
}

class Group extends Graph {
  constructor(array) {
    super()
    this.data = array
    this._init()
    this.lineWidth = 1
    this.backgroudColor = 'transparent'
  }

  _init() {
    this.xMap = []
    this.yMap = []
    this.data.forEach(shape => {
      this.xMap.push(shape.minX)
      this.xMap.push(shape.maxX)
      this.yMap.push(shape.minY)
      this.yMap.push(shape.maxY)
    })
    this.startX = Math.min(...this.xMap)
    this.startY = Math.min(...this.yMap)
    this.endX = Math.max(...this.xMap)
    this.endY = Math.max(...this.yMap)
  }

  move(distanceX = 0, distanceY = 0) {
    if (distanceX === 0 && distanceY === 0) return
    this.data.forEach(e => {
      e.startX += distanceX
      e.startY += distanceY
      e.endX += distanceX
      e.endY += distanceY
    })
  }

  clone() {
    const instance = new Group(this.data)
    instance.selector = this.selector
    return instance
  }

  draw(ctx) {
    if (!ctx) return
    ctx.beginPath()

    ctx.moveTo(this.minX * dpr, this.minY * dpr)
    ctx.lineTo(this.maxX * dpr, this.minY * dpr)
    ctx.lineTo(this.maxX * dpr, this.maxY * dpr)
    ctx.lineTo(this.minX * dpr, this.maxY * dpr)
    ctx.lineTo(this.minX * dpr, this.minY * dpr)
    this.drawStrokeOrFill(ctx)

    this.drawStrokeStyle(ctx)
  }

}

class Rectangle extends Graph {
  constructor(startX, startY, color) {
    super()
    this.color = color
    this.startX = startX
    this.startY = startY
    this.endX = startX
    this.endY = startY
  }

  get minX() {
    return Math.min(this.startX, this.endX)
  }

  get maxX() {
    return Math.max(this.startX, this.endX)
  }

  get minY() {
    return Math.min(this.startY, this.endY)
  }

  get maxY() {
    return Math.max(this.startY, this.endY)
  }

  get width() {
    return this.maxX - this.minX
  }

  get height() {
    return this.maxY - this.minY
  }

  clone() {
    const instance = new Rectangle(this.startX, this.startY)
    instance.endX = this.endX
    instance.endY = this.endY
    return instance
  }

  isValid() {
    return this.startX !== this.endX && this.startY !== this.endY
  }

  draw(ctx) {
    if (!ctx) return
    ctx.beginPath()
    this.drawLineDash(ctx)

    ctx.moveTo(this.minX * dpr, this.minY * dpr)
    ctx.lineTo(this.maxX * dpr, this.minY * dpr)
    ctx.lineTo(this.maxX * dpr, this.maxY * dpr)
    ctx.lineTo(this.minX * dpr, this.maxY * dpr)
    ctx.lineTo(this.minX * dpr, this.minY * dpr)
    this.drawStrokeOrFill(ctx)

    this.drawStrokeStyle(ctx)
  }

  isInside(x, y) {
    return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY
  }

}

class Diamond extends Rectangle {
  constructor(startX, startY, color) {
    super(startX, startY, color)
  }

  clone() {
    const instance = new Diamond(this.startX, this.startY, this.color)
    instance.endX = this.endX
    instance.endY = this.endY
    return instance
  }

  draw(ctx) {
    if (!ctx) return
    this.calcPoints()
    ctx.beginPath()
    this.drawLineDash(ctx)
    for (let i = 0; i < this.points.length; i++) {
      const point = this.points[i]
      if (i === 0) {
        ctx.moveTo(point[0] * dpr, point[1] * dpr)
      } else {
        ctx.lineTo(point[0] * dpr, point[1] * dpr)
      }
    }
    ctx.closePath()
    this.drawStrokeOrFill(ctx)

    this.drawStrokeStyle(ctx)
  }

  calcPoints() {
    const centerX = this.minX + this.width / 2
    const centerY = this.minY + this.height / 2
    // 计算菱形四个顶点
    this.points = [
      [centerX, this.minY],
      [this.maxX, centerY],
      [centerX, this.maxY],
      [this.minX, centerY]
    ]
  }

  isInside(x, y) {
    return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY
  }
}

class Selector extends Rectangle {
  constructor(startX, startY, color) {
    super(startX, startY, color)
    this.selector = true
    this.pattern = [3, 3]
  }

  draw(ctx) {
    if (!ctx) return

    ctx.beginPath()
    ctx.setLineDash(this.pattern.map(v => v * dpr))

    ctx.moveTo(this.minX * dpr, this.minY * dpr)
    ctx.lineTo(this.maxX * dpr, this.minY * dpr)
    ctx.lineTo(this.maxX * dpr, this.maxY * dpr)
    ctx.lineTo(this.minX * dpr, this.maxY * dpr)
    ctx.lineTo(this.minX * dpr, this.minY * dpr)

    // 描边
    ctx.strokeStyle = 'rgba(0, 0, 0, 0.7)'
    ctx.lineWidth = 1 * dpr
    ctx.lineCap = 'round'
    ctx.stroke()

    // 如果要切换回至实线模式，将 dash list 设置为一个空数组即可
    ctx.setLineDash([])
  }
}

class Circle extends Graph {
  constructor(startX, startY, color) {
    super()
    this.color = color
    this.startX = startX
    this.startY = startY
    this.endX = startX
    this.endY = startY
  }
  // (x-a)^2 + (y-b)^2 = r^2
  get r() {
    return Math.sqrt((this.endX - this.startX) ** 2 + (this.endY - this.startY) ** 2)
  }

  get width() {
    return this.maxX - this.minX
  }
  get height() {
    return this.maxY - this.minY
  }

  isValid() {
    return this.startX !== this.endX && this.startY !== this.endY
  }

  clone() {
    const instance = new Circle(this.startX, this.startY)
    instance.endX = this.endX
    instance.endY = this.endY
    return instance
  }

  draw(ctx) {
    if (!ctx) return
    ctx.beginPath()
    this.drawLineDash(ctx)

    if (this.E && this.E.shiftKey) {
      ctx.arc(this.startX * dpr, this.startY * dpr, this.r * dpr, 0, 2 * Math.PI)
    } else {
      const wr = this.width / 2
      const hr = this.height / 2
      const centerX = this.minX + wr
      const centerY = this.minY + hr
      ctx.ellipse(centerX * dpr, centerY * dpr, wr * dpr, hr * dpr, 0, 0, 2 * Math.PI)
    }
    this.drawStrokeOrFill(ctx)

    this.drawStrokeStyle(ctx)
  }

  isInside(x, y) {
    const dx = this.startX - x
    const dy = this.startY - y
    return dx * dx + dy * dy <= this.r * this.r
  }
}

class Line extends Graph {
  constructor(startX, startY, color) {
    super()
    this.color = color
    this.startX = startX
    this.startY = startY
    this.endX = startX
    this.endY = startY
  }

  get width() { return this.maxX - this.minX }
  get height() { return this.maxY - this.minY }
  get k() {
    const denominator = this.endX - this.startX
    if (denominator === 0) return null
    return (this.endY - this.startY) / denominator
  }
  // 求已知直线的上下30°夹角的两条直线斜率 k
  get ok() {
    const k = this.k
    if (k === null) return { k1: 2, k2: -2 }
    const a = Math.sqrt(3) / 3
    const k1 = (-1 * k - a) / (k * a - 1)
    const k2 = (k - a) / (k * a + 1)
    return { k1, k2 }
  }
  // 获取两点所在的直线点斜式的公式为：y = k(x - x1) + y1
  formula(k, x) {
    return k * (x - this.endX) + this.endY
  }

  /**
   * 计算直线和圆的交点坐标
   * @param {Nubmer} otherK 小箭头所在直线的斜率
   * @return Array - 返回交点坐标数组
   */
  findIntersectionPoints(otherK) {
    const radius = this.arrow?.radius || 6
    const k = otherK
    const b = -k * this.endX + this.endY
    const a = 1 + k ** 2
    const B = 2 * (k * b - k * this.endY - this.endX)
    const c = this.endX ** 2 + b ** 2 - radius ** 2 - 2 * b * this.endY + this.endY ** 2

    // 圆的方程：(x - a) ** 2 + (y - b) ** 2 = r ** 2
    // 将 y = x 代入圆的方程得到的新方程
    // 即 (x - a) ** 2 + (x - b) ** 2 = r ** 2
    let discriminant = B ** 2 - 4 * a * c

    if (discriminant < 0) {
      console.log("直线与圆没有交点")
      return []
    } else if (discriminant === 0) {
      const x = -B / (2 * a)
      const y = this.formula(k, x)
      console.log(`直线与圆的交点为 (${x}, ${y})`)
      return [x, y]
    } else {
      const x1 = (-B + Math.sqrt(discriminant)) / (2 * a)
      const y1 = this.formula(k, x1)
      const x2 = (-B - Math.sqrt(discriminant)) / (2 * a)
      const y2 = this.formula(k, x2)
      console.log(`直线与圆的交点为 A(${x1}, ${y1})，B(${x2}, ${y2})`)
      this.points = [
        [x1, y1], [x2, y2]
      ]

      if (this.k === null) {
        return y1 >= this.minY && y1 <= this.maxY ? [x1, y1] : [x2, y2]
      } else if (this.k === 0) {
        return x1 >= this.minX && x1 <= this.maxX ? [x1, y1] : [x2, y2]
      } else if (this.k >= -1 && this.k <= 1) {
        return x1 >= this.minX && x1 <= this.maxX ? [x1, y1] : [x2, y2]
      } else {
        return y1 >= this.minY && y1 <= this.maxY ? [x1, y1] : [x2, y2]
      }
    }
  }

  isValid() {
    return this.startX !== this.endX && this.startY !== this.endY
  }

  clone() {
    const instance = new Line(this.startX, this.startY, this.color)
    instance.endX = this.endX
    instance.endY = this.endY
    return instance
  }

  drawArrow(ctx) {
    // 画直线的两个小箭头的短线
    for (let k in this.ok) {
      const res = this.findIntersectionPoints(this.ok[k])
      this.point = res
      if (res.length) {
        ctx.beginPath()
        this.drawLineDash(ctx)

        const [a, b] = res
        ctx.moveTo(this.endX * dpr, this.endY * dpr)
        ctx.lineTo(a * dpr, b * dpr)
        this.drawStrokeStyle(ctx)
      }
    }
  }

  draw(ctx) {
    if (!ctx) return
    if (this.arrow.right === 'arrow-end') {
      this.drawArrow(ctx)
    }

    ctx.beginPath()
    this.drawLineDash(ctx)

    ctx.moveTo(this.startX * dpr, this.startY * dpr)
    ctx.lineTo(this.endX * dpr, this.endY * dpr)

    this.drawStrokeStyle(ctx)
  }

  // 判断某坐标是否在直线上
  isInside(x, y) {
    const range = 2 * dpr
    const b = this.startY - this.k * this.startX
    // 直线方程 y = k * x + b
    const result = this.k * x + b
    return result >= y - range && result <= y + range
  }
}

class BezierLine extends Line {
  constructor(startX, startY, color) {
    super(startX, startY, color)
  }

  get cp1X() {
    if (this.maxX - this.minX < 3 * dpr) {
      return this.startX
    }
    else if (this.startX < this.endX && this.startY > this.endY) {
      return this.width < this.height ? this.maxX : this.minX
    }
    else if (this.startX < this.endX && this.startY < this.endY) {
      return this.width < this.height ? this.maxX : this.minX
    }
    else if (this.startX > this.endX && this.startY < this.endY) {
      return this.width < this.height ? this.minX : this.maxX
    }
    else if (this.startX > this.endX && this.startY > this.endY) {
      return this.width < this.height ? this.minX : this.maxX
    }
  }
  get cp1Y() {
    if (this.maxY - this.minY < 3 * dpr) {
      return this.startY
    }
    else if (this.startX < this.endX && this.startY > this.endY) {
      return this.width < this.height ? this.maxY : this.minY
    }
    else if (this.startX < this.endX && this.startY < this.endY) {
      return this.width < this.height ? this.minY : this.maxY
    }
    else if (this.startX > this.endX && this.startY < this.endY) {
      return this.width < this.height ? this.minY : this.maxY
    }
    else if (this.startX > this.endX && this.startY > this.endY) {
      return this.width < this.height ? this.maxY : this.minY
    }
  }
  get cp2X() { return this.cp1X }
  get cp2Y() { return this.cp1Y }

  get k() {
    const denominator = this.endX - this.cp1X
    if (denominator === 0) {
      return null
    }
    return (this.endY - this.cp1Y) / (this.endX - this.cp1X)
  }

  isValid() {
    return this.startX !== this.endX && this.startY !== this.endY
  }

  clone() {
    const instance = new BezierLine(this.startX, this.startY, this.color)
    instance.endX = this.endX
    instance.endY = this.endY
    return instance
  }

  drawCpx(ctx) {
    ctx.beginPath()
    ctx.arc(this.cp1X * dpr, this.cp1Y * dpr, 3 * dpr, 0, 2 * Math.PI)
    ctx.fillStyle = 'red'
    ctx.fill()
  }

  draw(ctx) {
    if (!ctx) return
    // 绘制控制点
    // this.drawCpx(ctx)

    if (this.arrow?.right === 'arrow-end') {
      this.drawArrow(ctx)
    }

    ctx.beginPath()
    this.drawLineDash(ctx)

    ctx.moveTo(this.startX * dpr, this.startY * dpr)
    /** 一次贝塞尔曲线 */
    ctx.quadraticCurveTo(this.cp1X * dpr, this.cp1Y * dpr, this.endX * dpr, this.endY * dpr)
    /** 二次贝塞尔曲线 */
    // ctx.bezierCurveTo(this.cp1X * dpr, this.cp1Y * dpr, this.cp2X * dpr, this.cp2Y * dpr, this.endX * dpr, this.endY * dpr)

    this.drawStrokeStyle(ctx)
  }

  isInside(x, y) {
    const dx = this.startX - x
    const dy = this.startY - y
    const d = Math.sqrt(dx * dx + dy * dy)
    return d <= 10
  }
}

class ArrowLine extends Line {
  constructor(startX, startY, color) {
    super(startX, startY, color)
    this.arrowRadius = 5
  }

  // 求已知直线的上下30°夹角的两条直线斜率 k
  get ok() {
    const k = this.k
    const a = Math.sqrt(3) / 3
    const k1 = (-1 * k - a) / (k * a - 1)
    const k2 = (k - a) / (k * a + 1)
    return { k1, k2 }
  }

  // 获取两点所在的直线点斜式的公式为：y = k(x - x1) + y1
  formula(k, x) {
    return k * (x - this.endX) + this.endY
  }

  // 计算直线和圆的交点坐标
  findIntersectionPoints(radius, slope) {
    const k = slope
    const b = -k * this.endX + this.endY
    const a = 1 + k ** 2
    const B = 2 * (k * b - k * this.endY - this.endX)
    const c = this.endX ** 2 + b ** 2 - radius ** 2 - 2 * b * this.endY + this.endY ** 2

    // 圆的方程：(x - a) ** 2 + (y - b) ** 2 = r ** 2
    // 将 y = x 代入圆的方程得到的新方程
    // 即 (x - a) ** 2 + (x - b) ** 2 = r ** 2
    let discriminant = B ** 2 - 4 * a * c

    if (discriminant < 0) {
      console.log("直线与圆没有交点")
      return []
    } else if (discriminant === 0) {
      const x = -B / (2 * a)
      const y = this.formula(k, x)
      console.log(`直线与圆的交点为 (${x}, ${y})`)
      return [x, y]
    } else {
      const x1 = (-B + Math.sqrt(discriminant)) / (2 * a)
      const y1 = this.formula(k, x1)
      const x2 = (-B - Math.sqrt(discriminant)) / (2 * a)
      const y2 = this.formula(k, x2)
      console.log(`直线与圆的交点为 A(${x1}, ${y1})，B(${x2}, ${y2})`)
      this.points = [
        [x1, y1], [x2, y2]
      ]

      if (this.k >= -1 && this.k <= 1) {
        return x1 >= this.minX && x1 <= this.maxX ? [x1, y1] : [x2, y2]
      } else {
        return y1 >= this.minY && y1 <= this.maxY ? [x1, y1] : [x2, y2]
      }
    }
  }

  clone() {
    const instance = new ArrowLine(this.startX, this.startY, this.color)
    instance.endX = this.endX
    instance.endY = this.endY
    return instance
  }

  draw(ctx) {
    if (!ctx) return
    // 画直线的两个小箭头的短线
    for (let k in this.ok) {
      const res = this.findIntersectionPoints(this.arrowRadius * dpr, this.ok[k])
      this.point = res

      if (res.length) {
        ctx.beginPath()
        this.drawLineDash(ctx)

        const [a, b] = res
        ctx.moveTo(this.endX * dpr, this.endY * dpr)
        ctx.lineTo(a * dpr, b * dpr)
        this.drawStrokeStyle(ctx)
      }
    }

    // 画直线
    ctx.beginPath()
    this.drawLineDash(ctx)

    ctx.moveTo(this.startX * dpr, this.startY * dpr)
    ctx.lineTo(this.endX * dpr, this.endY * dpr)
    this.drawStrokeOrFill(ctx)

    this.drawStrokeStyle(ctx)
  }

}

class Textbox extends Rectangle {
  isTextbox = true
  constructor(startX, startY, text = 'Text', color) {
    super(startX, startY, color)
    this.text = text
    this.fontSize = 36 // 18px
    this.padding = 10 // 8px
    this.fontFamily = 'Arial'
    this.textBaseline = 'alphabetic'
  }

  blur(el, fn) {
    el.addEventListener('blur', (e) => {
      const { target } = e
      this.text = target.value
      el.remove()
      fn(this)
    })
  }

  clone() {
    const instance = new Textbox(this.startX, this.startY, this.text, this.color)
    instance.endX = this.endX
    instance.endY = this.endY
    return instance
  }

  draw(ctx) {
    if (!ctx) return
    ctx.font = `${this.fontSize * dpr}px ${this.fontFamily}`
    ctx.fillStyle = this.strokeStyle
    const posX = this.startX + this.padding / 2
    const posY = this.startY + this.fontSize
    ctx.fillText(this.text, posX * dpr, posY * dpr)

    const width = ctx.measureText(this.text).width / dpr
    console.log("%c Line:537 🍿 width", "color:#fca650", width)

    this.endX = this.startX + this.padding + width
    this.endY = this.startY + this.padding + this.fontSize

    // 画文字外框的矩形
    // super.draw(ctx)
  }

}

class Brush extends Graph {
  points = []
  constructor(startX, startY, color) {
    super()
    this.startX = startX
    this.startY = startY
    this.endX = startX
    this.endY = startY
    this.points.push([startX, startY])

    const proxyHander = {
      set: (target, key, value) => {
        if (key === 'endY') {
          target.points.push([target.endX, value])
        }
        target[key] = value
        return true
      }
    }
    return new Proxy(this, proxyHander)
  }

  draw(ctx) {
    if (!ctx) return
    ctx.beginPath()
    this.drawLineDash(ctx)

    const len = this.points.length
    for (let i = 0; i < len; i++) {
      const [x, y] = this.points[i]
      if (i === 0) {
        ctx.moveTo(x * dpr, y * dpr)
      } else {
        ctx.lineTo(x * dpr, y * dpr)
      }
    }

    this.drawStrokeStyle(ctx)
  }

  isInside(x, y) {
    return false
  }
}

window.Shape = Shape
window.Selector = Selector
window.Group = Group
window.Rectangle = Rectangle
window.Diamond = Diamond
window.Circle = Circle
window.Line = Line
window.ArrowLine = ArrowLine
window.BezierLine = BezierLine
window.Textbox = Textbox
window.Brush = Brush
